using System;
using System.Collections.Generic;

namespace LoRel {
    public class Index {
        private readonly Proxy m_BucketProxy;
        private readonly Dictionary< Key, IndexBucket > m_IndexBucketLookup;
        private readonly List<IndexChangeDelegate> m_RegisteredForAll;
        private readonly Proxy m_UniqueProxy;
        private readonly bool m_IsUnique;

        public Index( Proxy unique_proxy, Proxy bucket_proxy ) {
            m_UniqueProxy = unique_proxy;
            m_BucketProxy = bucket_proxy;
            if (m_BucketProxy.Equals(m_UniqueProxy)) m_IsUnique = true;
            m_IndexBucketLookup = new Dictionary< Key, IndexBucket >();
            m_RegisteredForAll = new List< IndexChangeDelegate >();
        }

        public NullReport GetKey( Object external_instance, out Key key ) { return m_BucketProxy.GetKey( external_instance, out key ); }

        public bool TryGetBucket( Key key, out IndexBucket bucket ) { return m_IndexBucketLookup.TryGetValue( key, out bucket ); }

        #region Add/Remove/Update/Clear

        //private void HandleAdd( TableChangeArgs args ) {
        //    foreach( TableChangeItem item in args.ChangedItems ) {
        //        Key key;
        //        if( m_BucketProxy.GetKey( item.NewValue.ExternalData, out key ) == false ) {
        //            throw new NotImplementedException();
        //        }
        //        IndexBucket index_bucket;
        //        if( m_IndexBucketLookup.TryGetValue( key, out index_bucket ) == false ) {
        //            index_bucket = new IndexBucket( key );
        //            m_IndexBucketLookup.Add( key, index_bucket );
        //        }
        //        index_bucket.Add( item.NewValue );
        //    }
        //}

        //private void HandleRemove( TableChangeArgs args ) {
        //    foreach( TableChangeItem item in args.ChangedItems ) {
        //        Key key;
        //        if( m_BucketProxy.GetKey( item.OldValue.ExternalData, out key ) == false ) {
        //            throw new NotImplementedException();
        //        }
        //        IndexBucket index_bucket;
        //        if( m_IndexBucketLookup.TryGetValue( key, out index_bucket ) == false ) {
        //            throw new NotImplementedException( "Should never occur." );
        //        }
        //        index_bucket.Remove( item.OldValue );
        //        if( index_bucket.Count == 0 ) {
        //            throw new NotImplementedException( "Should notify?" );
        //        }
        //    }
        //}

        //private void HandleUpdate( TableChangeArgs args ) {
        //    List< TableChangeItem > add = null, remove = null;

        //    foreach( TableChangeItem item in args.ChangedItems ) {
        //        Key new_key;
        //        Key old_key;

        //        if( m_BucketProxy.GetKey( item.NewValue, out new_key ) == false
        //            || m_BucketProxy.GetKey( item.NewValue, out old_key ) == false ) {
        //            throw new NotImplementedException(
        //                "Data failure should not throw, instead it should have application flow" );
        //        }

        //        if( old_key != new_key ) {
        //            if( add == null ) {
        //                add = new List< TableChangeItem >();
        //                remove = new List< TableChangeItem >();
        //            }

        //            add.Add( new TableChangeItem( item.NewValue, null ) );
        //            remove.Add( new TableChangeItem( item.OldValue, null ) );
        //        } else {
        //            IndexBucket index_bucket;
        //            if( m_IndexBucketLookup.TryGetValue( new_key, out index_bucket ) == false ) {
        //                throw new NotImplementedException( "Should never occur." );
        //            }
        //            index_bucket.Update( item.NewValue );
        //        }
        //    }
        //    if( add != null ) {
        //        TableChangeArgs add_args = new TableChangeArgs( ChangeType.Add, add.Count, add );
        //        TableChangeArgs remove_args = new TableChangeArgs( ChangeType.Remove, remove.Count, remove );
        //        HandleRemove( remove_args );
        //        HandleAdd( add_args );
        //    }
        //}

        //private void HandleClear() { m_IndexBucketLookup.Clear(); }

        #endregion

        private bool GetPartitionedChanges( TableChangeArgs args,
            out Dictionary<Key, List<TableChangeItem>> result,
            out Dictionary<Key, List<TableChangeItem>> updates_as_remove,
            out Dictionary<Key, List<TableChangeItem>> updates_as_add
            ) {
            result = new Dictionary< Key, List< TableChangeItem > >();
            updates_as_remove = null;
            updates_as_add = null;

            switch( args.ChangeType ) {
                case ChangeType.Add:
                    foreach (TableChangeItem item in args.ChangedItems)
                    {
                        Key key;
                        List< TableChangeItem > list;

                        switch ( m_BucketProxy.GetKey( item.NewValue.ExternalData, out key ) )
                        {
                            case NullReport.AllPopulated:
                                break;
                            case NullReport.SomeNull:
                            case NullReport.AllNull:
                                throw new NotImplementedException();
                            default:
                                throw new ArgumentOutOfRangeException();
                        }

                        if (result.TryGetValue(key, out list) == false)
                        {
                            list = new List<TableChangeItem>();
                            result.Add(key, list);
                        }
                        list.Add( item );
                    }
                    break;
                case ChangeType.Remove:
                    throw new NotImplementedException();
                    break;
                case ChangeType.Update:
                    foreach (TableChangeItem item in args.ChangedItems)
                    {
                        Key new_key, old_key;
                        List<TableChangeItem> list;

                        switch (m_BucketProxy.GetKey(item.NewValue.ExternalData, out new_key))
                        {
                            case NullReport.AllPopulated:
                                break;
                            case NullReport.SomeNull:
                            case NullReport.AllNull:
                                throw new NotImplementedException();
                            default:
                                throw new ArgumentOutOfRangeException();
                        }

                        switch (m_BucketProxy.GetKey(item.OldValue.ExternalData, out old_key))
                        {
                            case NullReport.AllPopulated:
                                break;
                            case NullReport.SomeNull:
                            case NullReport.AllNull:
                                throw new NotImplementedException();
                            default:
                                throw new ArgumentOutOfRangeException();
                        }
                        if( new_key != old_key ) {
                            if( updates_as_remove == null ) {
                                updates_as_remove = new Dictionary< Key, List< TableChangeItem > >();
                                updates_as_add = new Dictionary< Key, List< TableChangeItem > >();
                            }
                            List< TableChangeItem > remove_list;
                            List< TableChangeItem > add_list;
                            if( updates_as_remove.TryGetValue( old_key, out remove_list ) == false ) {
                                remove_list = new List< TableChangeItem >();
                                updates_as_remove.Add( old_key, remove_list );
                            }
                            
                            // can these happen seperately? can add exist w/o remove? I don't think so.
                            if (updates_as_remove.TryGetValue(new_key, out add_list) == false) {
                                add_list = new List< TableChangeItem >();
                                updates_as_add.Add( new_key, add_list );
                            }

                            // special case for table change item? we can identify a split addition/removal? interesting.
                            remove_list.Add(new TableChangeItem(item.NewValue, item.OldValue));
                            add_list.Add(new TableChangeItem(item.NewValue, item.OldValue));
                        } else {
                            if (result.TryGetValue(new_key, out list) == false)
                            {
                                list = new List<TableChangeItem>();
                                result.Add(new_key, list);
                            }
                            list.Add(item);
                        }
                    }
                    // all of them are a bit special, since we actually ARE reinterpreting (?)
                    break;
                case ChangeType.Clear:
                    throw new NotImplementedException();
                default:
                    throw new ArgumentOutOfRangeException();
            }
            return updates_as_remove != null;
        }

        /// <summary> 
        /// Converts underlying TableChangeItem to index specific TableChangeItem. 
        /// This is related to nullability.
        /// </summary>
        /// <param name="partitioned"></param>
        /// <returns></returns>
        private Dictionary<Key, TableChangeArgs> GetProcessedArgs( ChangeType type, Dictionary<Key, List<TableChangeItem>> partitioned) {
            Dictionary<Key, TableChangeArgs > args = new Dictionary< Key, TableChangeArgs >();
            foreach( KeyValuePair<Key, List<TableChangeItem> > item in partitioned ) {
                args.Add( item.Key, new TableChangeArgs( type, item.Value.Count, item.Value ) );
            }
            return args;
        }

        private IndexBucket CreateBucket(Key key)
        {
            IndexBucket bucket = new IndexBucket(key);

            FireIndexBucketCreated( bucket );

            foreach (IndexChangeDelegate listener in m_RegisteredForAll)
            {
                bucket.ChangePending += listener;
            }
            return bucket;
        }

        private IndexBucket GetOrCreateBucket( Key value ) {
            IndexBucket bucket;
            if (TryGetBucket(value, out bucket) == false) {
                bucket = CreateBucket( value );
                m_IndexBucketLookup.Add( value, bucket );
            }
            return bucket;
        }
        public IndexBucket GetBucket(Key key) { return GetOrCreateBucket( key ); }
        public void EnqueueUpdate(TableChangeArgs args) {
            Dictionary<Key, List<TableChangeItem>> partitioned;
            Dictionary<Key, List<TableChangeItem>> partitioned_remove;
            Dictionary<Key, List<TableChangeItem>> partitioned_add;
            if( GetPartitionedChanges( args, out partitioned, out partitioned_remove, out partitioned_add ) ) {

                foreach (KeyValuePair<Key, TableChangeArgs> pair in GetProcessedArgs(ChangeType.Remove, partitioned_remove))
                {
                    GetOrCreateBucket(pair.Key).EnqueueUpdate(pair.Value);
                }

                foreach (KeyValuePair<Key, TableChangeArgs> pair in GetProcessedArgs(ChangeType.Add, partitioned_add))
                {
                    GetOrCreateBucket(pair.Key).EnqueueUpdate(pair.Value);
                }
            }

            
            foreach (KeyValuePair<Key, TableChangeArgs> pair in GetProcessedArgs( args.ChangeType, partitioned) )
            {
                //if( IndexChangePending != null ){ IndexChangePending( pair.Key, pair.Value );}
                GetOrCreateBucket(pair.Key).EnqueueUpdate(pair.Value);
            }
        }

        public void EnqueueUpdate( Key value, TableChangeArgs args ) {
            EnqueueUpdate( args );
        }


        public void RegisterListener( IndexChangeDelegate handler ) {
            m_RegisteredForAll.Add( handler );
            foreach( KeyValuePair<Key, IndexBucket> pair in m_IndexBucketLookup ) {
                pair.Value.ChangePending += handler;
            }
        }

        public void DeRegisterListener( IndexChangeDelegate handler ) {
            m_RegisteredForAll.Remove( handler );
            foreach( KeyValuePair<Key, IndexBucket> pair in m_IndexBucketLookup ) {
                pair.Value.ChangePending -= handler;
            }
        }

        public void RegisterListener( Key key, IndexChangeDelegate handler ) {
            GetOrCreateBucket(key).ChangePending += (handler);
        }

        public event IndexChangeDelegate IndexChangePending {
            add { RegisterListener( value ); } 
            remove { DeRegisterListener( value ); }
        }

        private void FireIndexBucketCreated( IndexBucket bucket ) {
            if (IndexBucketCreated != null) {
                IndexBucketCreated( this, bucket );
            }
        }
        public event IndexBucketCreatedHandler IndexBucketCreated;
        
    }

}